include("jclass.utils");

namespace("jclass.utils.zip")
({
    /**
	 * Computes CRC32 data checksum of a data stream.
	 * The actual CRC32 algorithm is described in RFC 1952
	 * (GZIP file format specification version 4.3).
	 */
    CRC32 : jClass({
        private : {
            crc : 0,
            crcTable : makeCrcTable,
            makeCrcTable : function() {
                var crcTable = new Array(256);
                for (var n = 0; n < 256; n++) {
                    var c = n;
                    for (var k = 8; --k >= 0; ) {
                        if((c & 1) !== 0) c = 0xedb88320 ^ (c >>> 1);
                        else c = c >>> 1;
                    }
                    crcTable[n] = c;
                }
                return crcTable;
            }
        },
        public : {
            /**
             * Returns the CRC32 data checksum computed so far.
             */
            getValue : function() { return crc & 0xffffffff; },
            /**
             * Resets the CRC32 data checksum as if no update was ever called.
             */
            reset : function() { crc = 0; },
            /**
             * Adds the complete byte array to the data checksum.
             *
             * @param buf the buffer which contains the data
             */
            update : function(buf) {
                var off = 0;
                var len = buf.length;
                var c = ~crc;
                while(--len >= 0) c = crcTable[(c ^ buf.readByte()) & 0xff] ^ (c >>> 8);
                crc = ~c;
            }
        }
    }),
    /**
	 * Inflater is used to decompress data that has been compressed according
	 * to the "deflate" standard described in rfc1950.
	 */
    Inflater : jClass({
        private : {
            // maximum bits in a code
            MAXBITS : 15,
            // maximum number of literal/length codes
            MAXLCODES : 286,
            // maximum number of distance codes
            MAXDCODES : 30,
            // maximum codes lengths to read (MAXLCODES + MAXDCODES)
            MAXCODES : 316,
            // number of fixed literal/length codes
            FIXLCODES : 288,
            // Size base for length codes 257..285
            LENS : [3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258],
            // Extra bits for length codes 257..285
            LEXT : [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0],
            // Offset base for distance codes 0..29
            DISTS : [1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577],
            // Extra bits for distance codes 0..29
            DEXT : [ 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13],
            // ByteArray of inpput buffer
            inbuf : undefined,
            // bytes read so far
            incnt: 0,
            // bit buffer
            bitbuf : 0,
            // number of bits in buffer
            bitcnt : 0,
            // Huffman code decoding tables
            lencode : undefined,
            distcode: undefined,

            bits : function(need) {
                var val = bitbuf;
                while(bitcnt < need) {
                    if (incnt == inbuf.length) throw new Error('available inflate data did not terminate', 2);
                    val |= inbuf[incnt++] << bitcnt;
                    bitcnt += 8;
                }
                bitbuf = val >> need;
                bitcnt -= need;
                return val & ((1 << need) - 1);
            },

            construct : function(h, length, n) {
                var offs = new Array();
                for (var len = 0; len <= MAXBITS; len++) h.count[len] = 0;
                for (var symbol = 0; symbol < n; symbol++) h.count[length[symbol]]++;
                if(h.count[0] == n) return 0;
                var left = 1;
                for(len = 1; len <= MAXBITS; len++) {
                    left <<= 1;
                    left -= h.count[len];
                    if(left < 0) return left;
                }
                offs[1] = 0;
                for(len = 1; len < MAXBITS; len++) offs[len + 1] = offs[len] + h.count[len];
                for(symbol = 0; symbol < n; symbol++)
                    if(length[symbol] !== 0) h.symbol[offs[length[symbol]]++] = symbol;
                return left;
            },

            decode : function(h) {
                var code = 0, first = 0, index = 0;
                for(var len = 1; len <= MAXBITS; len++) {
                    code |= bits(1);
                    var count = h.count[len];
                    if(code < first + count) return h.symbol[index + (code - first)];
                    index += count;
                    first += count;
                    first <<= 1;
                    code <<= 1;
                }
                return -9; // ran out of codes
            },

            codes : function(buff) {
                do {
                    var symbol = decode(lencode);
                    if(symbol < 0) return symbol;
                    if(symbol < 256) buf[buf.length] = symbol;
                    else if(symbol > 256) {
                        symbol -= 257;
                        if(symbol >= 29) throw new Error("invalid literal/length or distance code in fixed or dynamic block", -9);
                        var len = LENS[symbol] + bits(LEXT[symbol]);
                        symbol = decode(distcode);
                        if(symbol < 0) return symbol;
                        var dist = DISTS[symbol] + bits(DEXT[symbol]);
                        if(dist > buf.length) throw new Error("distance is too far back in fixed or dynamic block", -10);
                        while(len--) buf[buf.length] = buf[buf.length - dist];
                    }
                } while (symbol != 256);
                return 0;
            },

            stored : function(buf) {
                bitbuf = 0;
                bitcnt = 0;
                if(incnt + 4 > inbuf.length) throw new Error('available inflate data did not terminate', 2);
                var len = inbuf[incnt++];
                len |= inbuf[incnt++] << 8;
                if(inbuf[incnt++] != (~len & 0xff) || inbuf[incnt++] != ((~len >> 8) & 0xff))
                    throw new Error("stored block length did not match one's complement", -2);
                if(incnt + len > inbuf.length) throw new Error('available inflate data did not terminate', 2);
                while(len--) buf[buf.length] = inbuf[incnt++];
            },

            constructFixedTables : function() {
                var lengths = new Array();
                // literal/length table
                for(var symbol = 0; symbol < 144; symbol++) lengths[symbol] = 8;
                for(; symbol < 256; symbol++) lengths[symbol] = 9;
                for(; symbol < 280; symbol++) lengths[symbol] = 7;
                for(; symbol < FIXLCODES; symbol++) lengths[symbol] = 8;
                construct(lencode, lengths, FIXLCODES);
                for(symbol = 0; symbol < MAXDCODES; symbol++) lengths[symbol] = 5;
                construct(distcode, lengths, MAXDCODES);
            },

            constructDynamicTables : function() {
                var lengths = new Array(),
                    order = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15],
                    nlen = bits(5) + 257,
                    ndist = bits(5) + 1,
                    ncode = bits(4) + 4;
                if(nlen > MAXLCODES || ndist > MAXDCODES) throw new Error("dynamic block code description: too many length or distance codes", -3);
                for(var index = 0; index < ncode; index++) lengths[order[index]] = bits(3);
                for(; index < 19; index++) lengths[order[index]] = 0;
                var err = construct(lencode, lengths, 19);
                if(err !== 0) throw new Error("dynamic block code description: code lengths codes incomplete", -4);
                index = 0;
                while(index < nlen + ndist) {
                    var symbol = decode(lencode), len;
                    if(symbol < 16) lengths[index++] = symbol;
                    else {
                        len = 0;
                        if(symbol == 16) {
                            if(index === 0) throw new Error("dynamic block code description: repeat lengths with no first length", -5);
                            len = lengths[index - 1];
                            symbol = 3 + bits(2);
                        }
                        else if(symbol == 17) symbol = 3 + bits(3);
                        else symbol = 11 + bits(7);
                        if(index + symbol > nlen + ndist)
                            throw new Error("dynamic block code description: repeat more than specified lengths", -6);
                        while(symbol--) lengths[index++] = len;
                    }
                }

                err = construct(lencode, lengths, nlen);
                if(err < 0 || (err > 0 && nlen - lencode.count[0] != 1)) throw new Error("dynamic block code description: invalid literal/length code lengths", -7);
                err = construct(distcode, lengths.slice(nlen), ndist);
                if(err < 0 || (err > 0 && ndist - distcode.count[0] != 1)) throw new Error("dynamic block code description: invalid distance code lengths", -8);
                return err;
            }
        },
        public : {
            /**
             * Sets the input.
             *
             * @param buf The input (ByteArray).
             */
            setInput : function(buf) {
                inbuf = buf;
                inputf.endian = Endian.LITTLE;
            },
            /**
             * Inflates the compressed stream to the output buffer.
             *
             * @param buf The output buffer (ByteArray)
             */
            inflate : function(buf) {
                incnt = bitbuf = bitcnt = 0;
                var err = 0;
                do {
                    var last = bits(1),
				        type = bits(2);

                    if(type === 0) stored(buf); // uncompressed block
                    else if(type == 3) throw new Error('invalid block type (type == 3)', -1);
                    else { // compressed block
                        lencode = {count:[], symbol:[]};
                        distcode = {count:[], symbol:[]};
                        if(type == 1) constructFixedTables();
                        else if(type == 2) err = constructDynamicTables();
                        if(err !== 0) return err;
                        err = codes(buf);
                    }
                    if(err !== 0) break;
                } while (!last);
                return err;
            }
        }
    }),
    Deflater : jClass({

    })
});

namespace("jclass.utils.zip")
({
	/**
	 * This class represents a member of a zip archive.  ZipFile
	 * will give you instances of this class as information
	 * about the members in an archive.  On the other hand ZipOutput
	 * needs an instance of this class to create a new member.
	 *
	 */
    ZipEntry : jClass({
        /**
		 * Creates a zip entry with the given name.
		 * @param name the name. May include directory components separated
		 */
        constructor : function(name) {
            _name = name;
        },
        private : {
            dostime : 0,
            flag : 0,
            version : 0,
            offset : 0
        },
        public : {
            /**
             * Returns the entry name.  The path components in the entry are
             * always separated by slashes ('/').
             */
            name : Get(function() {
                return _name;
            }),
            /**
             * Time of last modification of the entry.
             */
            time : GetSet(function(){
                var d = new Date(((dostime >> 25) & 0x7f) + 1980,((dostime >> 21) & 0x0f) - 1,(dostime >> 16) & 0x1f,(dostime >> 11) & 0x1f,(dostime >> 5) & 0x3f,(dostime & 0x1f) << 1);
                return d.time;
            }, function(time) {
                var d = new Date(time);
                dostime = (d.fullYear - 1980 & 0x7f) << 25 | (d.month + 1) << 21 | d.day << 16 | d.hours << 11 | d.minutes << 5 | d.seconds >> 1;
            }),
            /**
             * Size of the uncompressed data.
             */
            size : GetSet(function(){ return _size; }, function(val) { _size = val; }),
            /**
             * Size of the compressed data.
             */
            compressedSize : GetSet(function(){ return _compressedSize; }, function(val) { _compressedSize = val; }),
            /**
             * CRC of the uncompressed data.
             */
            crc : GetSet(function(){ return _crc; }, function(val) { _crc = val; }),
            /**
             * Compression method.  Only DEFLATED and STORED are supported
             */
            method : GetSet(function(){ return _method; }, function(val) { _method = val; }),
            /**
             * Extra data
             */
            extra : GetSet(function(){ return _extra; }, function(val) { _extra = val; }),
            /**
             * Entry comment
             */
            comment : GetSet(function(){ return _comment; }, function(val) { _comment = val; }),

            isDirectory : function() {
                return _name.charAt(_name.length - 1) == '/';
            }
        }
    })
});

namespace("jclass.utils.zip")
({
    ZipFile : jClass({
        constructor : function(data) {
            buf = new jclass.utils.ByteArray();
            buf.endian = Endian.LITTLE;
            buf.writeBytes(data.readBytes(0, data.length));
            readEntries();
        },
        private : {
            buf : undefined, // bytearray
            entryList : undefined, // array
            entryTable : undefined, // Dict
            locOffsetTable : undefined, // dict

            readEntries : function() {
                readEND();
                entryTable = {};
                locOffsetTable = {};
                // read cen entries
                for(var i = 0; i < entryList.length; i++) {
                    var tmpbuf = new jclass.utils.ByteArray();
                    tmpbuf.endian = Endian.LITTLE;
                    tmpbuf.writeBytes(buf.readBytes(0, 46)); // CEN header size
                    if(tmpbuf.readUnsignedInt() != 0x02014b50)  // "PK\005\006"
                        throw new Error("readEntries::Invalid CEN header (bad signature)");
                    // handle filename
                    tmpbuf.position = 28;
                    var len = tmpbuf.readUnsignedShort();
                    if(len === 0) throw new Error("missing entry name");
                    var e = new jclass.utils.zip.ZipEntry(buf.readUTFBytes(len));
                    // handle extra field
                    len = tmpbuf.readUnsignedShort();
                    e.extra = new jclass.utils.ByteArray();
                    if(len > 0) buf.readBytes(e.extra, 0, len);
                    // handle file comment
                    buf.position += tmpbuf.readUnsignedShort();
                    // now get the remaining fields for the entry
                    tmpbuf.position = 6; // version needed to extract
                    e.version = tmpbuf.readUnsignedShort();
                    e.flag = tmpbuf.readUnsignedShort();
                    if ((e.flag & 1) == 1) throw new Error("readEntries::Encrypted ZIP entry not supported");
                    e.method = tmpbuf.readUnsignedShort();
                    e.dostime = tmpbuf.readUnsignedInt();
                    e.crc = tmpbuf.readUnsignedInt();
                    e.compressedSize = tmpbuf.readUnsignedInt();
                    e.size = tmpbuf.readUnsignedInt();
                    // add to entries and table
                    entryList[i] = e;
                    entryTable[e.name] = e;
                    // loc offset
                    tmpbuf.position = 42; // LOC HEADER
                    locOffsetTable[e.name] = tmpbuf.readUnsignedInt();
                }
            },
            readEND : function() {
                var b = new jclass.utils.ByteArray();
                b.endian = Endian.LITTLE;
                buf.position = findEND();
                b.writeBytes(buf.readBytes(0, 22))
                b.position = 10; // total number of entries
                entryList = [b.readUnsignedShort()];
                b.position = 16; // offset of first CEN header
                buf.position = b.readUnsignedInt();
            },
            findEND : function() {
                var i = buf.length - 22; // END header size
                var n = Math.max(0, i - 0xffff); // 0xffff is max zip file comment length
                for(i; i >= n; i--) {
                    if(buf[i] != 0x50) continue; // quick check that the byte is 'P'
                    buf.position = i;
                    if(buf.readUnsignedInt() == 0x06054b50) // "PK\005\006"
                        return i;
                }
                throw new Error("findEND::Invalid zip");
            }
        },
        public : {
            /**
             * Returns an array of all Zip entries in this Zip file.
             */
            entries : Get(function(){ return entryList; }),
            /**
             * Returns the number of entries in this zip file.
             */
            size : Get(function() { return entryList.length; }),
            /**
             * Searches for a zip entry in this archive with the given name.
             *
             * @param name The name. May contain directory components separated by
             * slashes ('/').
             * @return The zip entry, or null if no entry with that name exists.
             */
            getEntry : function(name) { return entryTable[name]; },
            /**
             * Creates a byte array reading the given zip entry as
             * uncompressed data.  Normally zip entry should be an entry
             * returned by getEntry() or entries().
             *
             * @param entry The entry to create a byte array for.
             * @return The byte array, or null if the requested entry does not exist.
             */
            getInput : function(entry) {
                buf.position = locOffsetTable[entry.name] + 30 - 2;
                var len = buf.readShort();
                buf.position += entry.name.length + len;
                var b1 = new jclass.utils.ByteArray();
                if(entry.compressedSize > 0) {
                    b1.writeBytes(buf.readBytes(0, entry.compressedSize));
                }
                switch(entry.method) {
                    case 0: // STORED
                        return b1;
                        break;
                    case 8: // DEFLATED
                        var b2 = new jclass.utils.ByteArray();
                        var inflater = new jclass.utils.zip.Inflater();
                        inflater.setInput(b1);
                        inflater.inflate(b2);
                        return b2;
                        break;
                    default:
                        throw new Error("zipEntry::getInput::Invalid compression method");
                }
            }
        }
    })
});

namespace("jclass.utils.zip")
({
    ZipOutput : jClass({
        constructor : function() {
            _buf.endian = Endian.LITTLE
        },
        private : {
            _entry : undefined, // ZipEntry
            _entries : undefined, // Array
            _names : undefined, // Array
            _def : new jclass.utils.zip.Deflater(),
            _crc : new jclass.utils.zip.CRC32(),
            _buf : new jclass.utils.ByteArray(),

            writeLOC : function(e) {
                _buf.writeInt(0x04034b50); // "PK\003\004"
                _buf.writeShort(e.version);
                _buf.writeShort(e.flag);
                _buf.writeShort(e.method);
                _buf.writeInt(e.dostime); // dostime
                if ((e.flag & 8) == 8) {
                    // store size, uncompressed size, and crc-32 in data descriptor immediately following compressed entry data
                    _buf.writeInt(0);
                    _buf.writeInt(0);
                    _buf.writeInt(0);
                } else {
                    _buf.writeInt(e.crc); // crc-32
                    _buf.writeInt(e.compressedSize); // compressed size
                    _buf.writeInt(e.size); // uncompressed size
                }
                _buf.writeShort(e.name.length);
                _buf.writeShort(e.extra !== undefined ? e.extra.length : 0);
                _buf.writeBytes(e.name);
                if (e.extra !== undefined) {
                    _buf.writeBytes(e.extra);
                }
            },
            writeEXT : function(e) {
                _buf.writeInt(0x04034b50); // "PK\003\004"
                _buf.writeInt(e.crc); // crc-32
                _buf.writeInt(e.compressedSize); // compressed size
                _buf.writeInt(e.size); // uncompressed size
            },
            writeCEN : function(e) {
                _buf.writedInt(0x04034b50); // CEN header signature
                _buf.writeShort(e.version); // version made by
                _buf.writeShort(e.version); // version needed to extract
                _buf.writeShort(e.flag); // general purpose bit flag
                _buf.writeShort(e.method); // compression method
                _buf.writeInt(e.dostime); // last modification time
                _buf.writeInt(e.crc); // crc-32
                _buf.writeInt(e.compressedSize); // compressed size
                _buf.writeInt(e.size); // uncompressed size
                _buf.writeShort(e.name.length);
                _buf.writeShort(e.extra !== undefined ? e.extra.length : 0);
                _buf.writeShort(e.comment !== undefined ? e.comment.length : 0);
                _buf.writeShort(0); // starting disk number
                _buf.writeShort(0); // internal file attributes (unused)
                _buf.writeInt(0); // external file attributes (unused)
                _buf.writeInt(e.offset); // relative offset of local header
                _buf.writeBytes(e.name);
                if (e.extra !== undefined) {
                    _buf.writeBytes(e.extra);
                }
                if (e.comment !== undefined) {
                    _buf.writeBytes(e.comment);
                }
            },
            writeEND : function(off, len) {
                _buf.writeInt(0x04034b50); // END record signature
                _buf.writeShort(0); // number of this disk
                _buf.writeShort(0); // central directory start disk
                _buf.writeShort(_entries.length); // number of directory entries on disk
                _buf.writeShort(_entries.length); // total number of directory entries
                _buf.writeInt(len); // length of central directory
                _buf.writeInt(off); // offset of central directory
                _buf.writeUTF(_comment); // zip file comment
            }
        },
        public : {
            /**
             * Returns the number of entries in this zip file.
             */
            size : Get(function() { return _entries.length; }),
            /**
             * Returns the byte array of the finished zip.
             */
            byteArray : Get(function() { _buf.position = 0; return _buf; }),
            
            comment : Set(function() { _comment = value; }),

            putNextEntry : function(e) {
                if(_entry !== undefined) this.closeEntry();
                if(e.dostime === 0) e.time = new Date().getTime();
                if (e.method == -1) e.method = 8; // DEFLATED
                switch(e.method) {
                    case 8: // DEFLATED
                        if (e.size == -1 || e.compressedSize == -1 || e.crc === 0) {
                            e.flag = 8; // store size, compressed size, and crc-32 in data descriptor, immediately following the compressed entry data
                        } else if (e.size != -1 && e.compressedSize != -1 && e.crc !== 0) {
                            e.flag = 0;  // store size, compressed size, and crc-32 in LOC header
                        } else {
                            throw new Error("putNextEntry::DEFLATED entry missing size, compressed size, or crc-32");
                        }
                        e.version = 20;
                        break;
                    case 0: // STORED
                        // compressed size, uncompressed size, and crc-32 must all be set for entries using STORED compression method
                        if (e.size == -1) {
                            e.size = e.compressedSize;
                        } else if (e.compressedSize == -1) {
                            e.compressedSize = e.size;
                        } else if (e.size != e.compressedSize) {
                            throw new Error("putNextEntry::STORED entry where compressed != uncompressed size");
                        }
                        if (e.size == -1 || e.crc === 0) {
                            throw new Error("putNextEntry::STORED entry missing size, compressed size, or crc-32");
                        }
                        e.version = 10;
                        e.flag = 0;
                        break;
                    default:
                        throw new Error("putNextEntry::Unsupported compression method");
                }
                e.offset = _buf.position;
                if (_names === undefined) _names = new Array();
                if (_entries === undefined) _entries = new Array();
                
                if (_names[e.name] !== undefined) {
                    throw new Error("putNextEntry::Duplicate entry: " + e.name);
                } else {
                    _names[e.name] = e;
                }
                writeLOC(e);
                _entries.push(e);
                _entry = e;
            },

            write : function(b) {
                if (_entry === undefined) {
                    throw new Error("write::No current ZIP entry");
                }
                switch (_entry.method) {
                    case 8: // DEFLATED
                        var cb = new jclass.utils.ByteArray();
                        _def.setInput(b);
                        _def.deflate(cb);
                        _buf.writeBytes(cb);
                        break;
                    case 0: // STORED
                        _buf.writeBytes(b.readBytes(0, b.length));
                        break;
                    default:
                        throw new Error("write::Invalid compression method");
                }
                _crc.update(b);
            },

            closeEntry : function() {
                var e = _entry;
                if(e !== undefined) {
                    switch (e.method) {
                        case 8: // DEFLATED
                            if ((e.flag & 8) === 0) {
                                if (e.size != _def.getBytesRead()) { // verify size, compressed size, and crc-32 settings
                                    throw new Error("closeEntry::Invalid entry size (expected " + e.size + " but got " + _def.getBytesRead() + " bytes)");
                                }
                                if (e.compressedSize != _def.getBytesWritten()) {
                                    throw new Error("closeEntry::Invalid entry compressed size (expected " + e.compressedSize + " but got " + _def.getBytesWritten() + " bytes)");
                                }
                                if (e.crc != _crc.getValue()) {
                                    throw new Error( "closeEntry::Invalid entry CRC-32 (expected 0x" + e.crc + " but got 0x" + _crc.getValue() + ")");
                                }
                            } else {
                                e.size = _def.getBytesRead();
                                e.compressedSize = _def.getBytesWritten();
                                e.crc = _crc.getValue();
                                writeEXT(e);
                            }
                            _def.reset();
                            break;
                        case 0: // STORED
                            // TODO:
                            break;
                        default:
                            throw new Error("closeEntry::Invalid compression method");
                    }
                    _crc.reset();
                    _entry = null;
                }
            },
            finish : function() {
                if(_entry !== undefined) closeEntry();
                if (_entries.length < 1) throw new Error("finish::ZIP file must have at least one entry");
                var off = _buf.position;
                for(var i = 0; i < _entries.length; i++) {
                    writeCEN(_entries[i]);
                }
                writeEND(off, _buf.position - off);
            }
        }
    })
});

namespace("jclass.utils.zip")
({
    ZipLoader : jClass({

    })
});